﻿using System;
using System.Collections.Generic;
using System.Windows.Forms;
using System.Text;
using ESRI.ArcGIS.ADF.BaseClasses;
using ESRI.ArcGIS.ADF.CATIDs;
using ESRI.ArcGIS.Controls;
using ESRI.ArcGIS.Carto;
using ESRI.ArcGIS.Display;
using ESRI.ArcGIS.Output;
using ESRI.ArcGIS.Geodatabase;
using ESRI.ArcGIS.Geometry;

namespace ActivityInfo.ArcGIS.Tiles
{
    class TileCoordinateSystem
    {
        /// <summary>
        /// The projected coordinate system of the map
        /// </summary>
        private readonly IProjectedCoordinateSystem projCS;

        /// <summary>
        /// The width, in projected units, of the projection
        /// </summary>
        private readonly double projectionWidth;

        /// <summary>
        /// The width of a tile in pixels
        /// </summary>
        private const int tileWidthPixels = 256;

        /// <summary>
        /// The target resolution, in dots-per-inch (=pixels-per-inch)
        /// </summary>
        private const int dpi = 96;

        private const double CmPerInch = 2.54;

        /// <summary>
        /// Initializes a Tiling system for a given Projected Coordinate 
        /// </summary>
        /// <param name="projCS"></param>
        public TileCoordinateSystem(IProjectedCoordinateSystem projCS)
        {
            this.projCS = projCS;
            this.projectionWidth = calculateProjectionWidth(projCS);
        }

        public TileCoordinateSystem(IMap map)
            : this((IProjectedCoordinateSystem)map.SpatialReference)
        {
        }

        private double calculateProjectionWidth(IProjectedCoordinateSystem projCS) 
        {
            IPoint west = new PointClass();
            west.PutCoords(-180, 0);
            IPoint east = new PointClass();
            east.PutCoords(+180, 0);

            ILine equator = new ESRI.ArcGIS.Geometry.LineClass();
            equator.PutCoords(west, east);
            equator.SpatialReference = CreateWGS84();
            equator.Project(projCS);

            return equator.Length;
        }

        public double ProjectionWidth
        {
            get
            {
                return projectionWidth;
            }
        }

        /// <summary>
        /// Calculates the bounds of a given tile in the projected coordinate system.
        /// </summary>
        /// <param name="zoomLevel"></param>
        /// <param name="tileX"></param>
        /// <param name="tileY"></param>
        /// <returns></returns>
        public IEnvelope ProjectedBoundsForTile(int zoomLevel, int tileX, int tileY)
        {
            IEnvelope geoBounds = new EnvelopeClass();
            double tileWidth = TileWidthForZoomLevel(zoomLevel);

            geoBounds.XMin = (tileX * tileWidth) - (projectionWidth / 2);
            geoBounds.XMax = geoBounds.XMin + tileWidth;
            geoBounds.YMax = (projectionWidth / 2) - (tileY * tileWidth);
            geoBounds.YMin = geoBounds.YMax - tileWidth;

            return geoBounds;
        }
        
        /// <summary>
        /// Calculates the width of the square tile in projected cooridinate system units
        /// for a given zoom level
        /// </summary>
        /// <param name="zoomLevel">The zoom level for which to calculate the tile size</param>
        /// <returns>The width of a tile in projected coordinate system units</returns>
        public double TileWidthForZoomLevel(int zoomLevel)
        {
            return projectionWidth / Math.Pow(2, zoomLevel);
        }

        private IGeographicCoordinateSystem CreateWGS84()
        {
            SpatialReferenceEnvironment srEnv = new SpatialReferenceEnvironmentClass();
            return srEnv.CreateGeographicCoordinateSystem(4326);
        }

        public IEnvelope TileBoundsForGeographicBounds(IEnvelope geoBounds, int zoomLevel)
        {
            // project the geographic bounds into the projected coordinated system
            IEnvelope projectedBounds = new EnvelopeClass();
            projectedBounds.PutCoords(geoBounds.XMin, geoBounds.YMin, geoBounds.XMax, geoBounds.YMax);
            projectedBounds.SpatialReference = CreateWGS84();
            projectedBounds.Project(projCS);

            //log(String.Format("Projected bounds = ({0}, {1}, {2}, {3}) in the {4}",
            //        projectedBounds.XMin,
            //        projectedBounds.YMin,
            //        projectedBounds.XMax,
            //        projectedBounds.YMax, spatialRef.Name));

            return TileBoundsForProjectedBounds(projectedBounds, zoomLevel);
        }

        /// <summary>
        /// Calculates the bounds, in tile coordinates, for a given projected bounds.
        /// </summary>
        /// <param name="geographicBounds">The projected bounds of the Area of Interest</param>
        /// <param name="zoomLevel">The Zoom level for which to calculate the tile bounds</param>
        /// <returns></returns>
        public IEnvelope TileBoundsForProjectedBounds(IEnvelope projectedBounds, int zoomLevel)
        {
            double tileWidth = TileWidthForZoomLevel(zoomLevel);

            IEnvelope tileBounds = new EnvelopeClass();
            tileBounds.XMin = Math.Floor(((projectionWidth / 2) + projectedBounds.XMin) / tileWidth);
            tileBounds.XMax = Math.Ceiling(((projectionWidth / 2) + projectedBounds.XMax) / tileWidth);
            tileBounds.YMin = Math.Floor(((projectionWidth / 2) - Math.Max(projectedBounds.YMin, -projectionWidth / 2)) / tileWidth);
            tileBounds.YMax = Math.Ceiling(((projectionWidth / 2) - Math.Min(projectedBounds.YMax, projectionWidth / 2)) / tileWidth);

            return tileBounds;
        }

        public double ScaleForZoomLevel(int zoomLevel)
        {
            // get the real size of a tile in meters             
            double realTileSize = TileWidthForZoomLevel(zoomLevel) *
                                        projCS.CoordinateUnit.MetersPerUnit;

            // get the mapped size of tile in meters
            double mappedTileSize = ((double)tileWidthPixels) / ((double)dpi) * CmPerInch / 100.0;

            return realTileSize / mappedTileSize;
        }
    }
}
